# module imports
from utility import merge, mget, to_timestamp
from filters import dates_filter, keys_filter, params_filter
from error import GlittrError

from weakref import WeakValueDictionary
# store instances of objects so that we don't get two of them
# at the same time
object_cache = WeakValueDictionary()

FLICKR_API = None
CACHING = True

# TODO remove auto info, make it like chris['username'], chris.photos
# can't assign to chris['username'], all set_ methods must have corresponding
# get_ methods, still uses internal info cache to keep track of everything
# make sure that ._info and .info are using the same information
# TODO egg/pudge/website/googlecode - only do cheeseshop when everything's tested and
# more or less complete

# TODO why doesn't p._get_current_user().photos ever using cached results?
# TODO why is the apicall made with .get_photos() before an results are grabbed from the generator?
# TODO merge depaginate into _action?
def depage(api_call, keys, **params):
    """
    Returns a generator that yields the items resulting from calling `api_call`
    and using the list `keys` to grab the desired items from the response.
    
    The main feature of this function is that it will keep calling `api_call`,
    incrementing the page parameter until it runs out of results.

    """
    # This function should probably not be used by any post methods, since it
    # would mean they would not be run until the first element of the generator was
    # grabbed
    page = params.get('page', 1)
    while True:
        params['page'] = page
        response = api_call(**params)

        items = response
        for key in keys:
            items = items[key]

        # if there is just one dictionary item result, only return it
        if isinstance(items, dict):
            yield items
        else:
            for item in items:
                yield item

        page = page + 1
        try: # the Flickr API is pretty forgiving about superfluous parameters
            if page > int(response[keys[0]]['pages']):
                break
        except KeyError:
            break

class BaseGlittr(dict):
    """
    Collection of common methods for other Glittr objects.

    If you attempt to access an attribute that is not defined on the object at
    instantiation and there is a .get_*() method, where * is the name of the
    attribute, it will be called, and the result returned.  If there is no
    .get_*() method, .get_info() is called (assuming it exists) and the result
    checked for the attribute that you attempted to access.

    If you attempt to assign to an attribute, and there is a .set_*() method,
    the attribute name and value are passed to that method.

    """
    params = {}

    def __new__(cls, *params, **kwargs):
        identifier = mget(kwargs, ['id', 'nsid'])
        
        # if we don't find an identifier, or if the object is not cacheable
        # don't mess with the object cache
        if not identifier or\
           not getattr(cls, '_cacheable', False):
            return object.__new__(cls, *params, **kwargs)

        # check the object cache to see if there is already one of
        # these in existence
        global object_cache
        obj_id = "%s-%s" % (cls.__name__, identifier)
        if object_cache.has_key(obj_id):
            obj = object_cache[obj_id]
            obj._info.update(kwargs)
        else:
            obj = object.__new__(cls, *params, **kwargs)
            object_cache[obj_id] = obj

        return obj

    def __init__(self, *params, **kwargs):
        """
        Make a basic glittr object, either id or nsid is required.  Some Flickr
        methods return one and not the other, but it doesn't seem that they
        ever mean different things.

        This method is called by the subclasses of BaseGlittr.

        """
        if len(params) == 1:
            kwargs['id'] = params[0]
        elif len(params) > 1:
            raise TypeError, "Multiple non-keyword arguments, can't decide what they are"

        identifier = mget(kwargs, ['id', 'nsid'])
        if identifier:
            self.id = identifier
        else:
            raise Exception, "Identifier required"

        self._info = {}
        self._info.update(kwargs)

    @property
    def flickr(self):
        """
        Get the currently active Flickr object.

        """
        return FLICKR_API

    def _merge_params(self, objects, params):
        params = merge(self._params.copy(),params)
        # convert any dates parameters into strings
        params = params_filter(params)

        if objects is None:
            objects = []
        for obj in objects:
            params.update(obj._params)
        return params

    def _get_current_user(self):
        if self.flickr.logged_in:
            return User(**self.flickr['auth']['user'])
        else:
            return None

    def _action(self, api_call, params, keys=None, constructor=None, objects=None, depaginate=False):
        """
        Perform the specified `api_call` (a function) using parameters from
        `params` and from the .params attributes of all objects in `objects`.
        Use `keys` to grab the desired data from the response like so:

            for key in keys:
                response = response[key]

        It's a common case that we have a `keys` argument like
        ['photos','photo'], so if `keys` is a string such as 'photo', the keys
        list ['photos', 'photo'] is generated from the string.

        If `constructor` is specified, it is used on each element of the response
        (obtained using the keys), and returned in the form of a generator.
        Multiple pages of results are handled in this manner, using the
        `depaginate` function.

        `depaginate` means that the results will be returned as a generator,
        and when the user attempts to grab more items than were returned in
        the first request, the next page (if it exists) is grabbed, and those
        results returned.

        """
        if keys == None:
            keys = []
        elif isinstance(keys, basestring):
            # automatically make plural form
            keys = [keys + 's', keys]

        # TODO clean up this constructor business
        def null_constructor(**kwargs):
            return kwargs
        if not constructor:
            constructor = null_constructor

        # merge the given params with the default ones from this object
        # and from all the objects passed in
        params = self._merge_params(objects, params)

        if depaginate:
            return (constructor(**e) for e in depage(api_call, keys, **params))
        else:
            response = api_call(**params)
            for key in keys:
                response = response[key]
            if isinstance(response, list):
                # generator to be consistent with depaginate
                return (constructor(**r) for r in response)
            elif isinstance(response, dict):
                return constructor(**response)
            else:
                if constructor == null_constructor:
                    return response
                else:
                    return constructor(response)
         
    def __getattr__(self, name):
        """
        If a non-existent attribute is accessed, query Flickr using the
        .get_info() method to see if the desired attribute is in there.  All
        attributes from the Flickr response are stored in the info cache.

        Only works if .get_info() has been defined for the object.

        """
        if name[0] == '_' or name[:4] == 'get_' or name[:4] == 'set_':
            raise AttributeError, "instance has no attribute '%s'" % name

        print "[[getattr]]:",name

        if CACHING:
            try:
                return self._info[name]
            except KeyError:
                try:
                    result = getattr(self, 'get_' + name)()
                    if hasattr(result,'next'): # probably a generator
                        result = list(result)
                    self.__setattr__(name, result)
                    return result
                except AttributeError:
                    pass
                
                if hasattr(self, 'get_info'):
                    if not hasattr(self, '_info_cached'):
                        try:
                            result = self.get_info()
                            for k,v in result.iteritems():
                                # don't set the value if there is already
                                # a pseudo-property
                                if hasattr(self, 'get_' + k):
                                    print "[[getattr]] eclipsed property:",k
                                    continue
                                else:
                                    self._info[k] = v
                            self._info_cached = True
                        except AttributeError:
                            pass
                    try:
                        return self._info[name]
                    except KeyError:
                        pass
        else:
            try:
                return self.get_info()[name]
            except (AttributeError, KeyError):
                pass

        raise AttributeError, "instance has no attribute '%s'" % name

    def __setattr__(self, name, value):
        if name[0] == '_' or name == 'id':
            super(BaseGlittr,self).__setattr__(name, value)
        else:
            try:
                fn = getattr(self, 'set_' + name)
            except AttributeError:
                pass
            else:
                if isinstance(value, dict):
                    fn(self,**value)
                elif isinstance(value, list):
                    fn(self,*value) 
                else:
                    fn(self,value)
                if CACHING:
                    self._info[name] = value

    @property
    def __members__(self):
        return self._info.keys()

    def __eq__(self, other):
        """
        Compare two Glittr objects, return True if they are derived
        from the same class and have the same id, False otherwise.

        """
        if isinstance(other, self.__class__):
            return self._get_id() == other._get_id()
        else:
            return False

    def _get_id(self):
        return self.id.encode('utf8')

    def _get_name(self):
        # can't use __getattr__ direclty, as that might cause it call something
        return mget(self._info, ['name', 'username', 'title', 'tag'], '??????').encode('utf8')

    def __repr__(self):
        """
        Return the default string representation for Glittr objects.

        """
        return "<Flickr %s: id='%s' '%s'>" %\
               (self.__class__.__name__, self._get_id(), self._get_name())

    def _subsequent_photos(self, direction, api_call, photo, params):
        while True:
            response = self._action(api_call, params, objects=(photo,))
            nextphoto = response[direction + 'photo']
            if nextphoto['id'] == 0:
                raise StopIteration
            photo = Photo(**nextphoto)
            yield photo
 

class Blog(BaseGlittr):
    _cacheable = True
    def __init__(self, *args, **kwargs):
        super(Blog, self).__init__(*args, **kwargs)
        self._params = {'blog_id':self.id}

    def post(self, photo, title='', description='', **params):
        """
        Post a new blog entry on this blog with the provided `photo`. Using
        `title` as the title of the entry and `description` as the content.

        api: flickr.blogs.postPhoto

        """
        params = merge(dict(title=title,description=description),params)
        return self._action(self.flickr.blogs.postPhoto, params, objects=(photo,))


class Comment(BaseGlittr):
    """
    Comments on photos and photosets are basically the same except they have
    different API calls dealing with them, so this base class is used by
    PhotoComment and PhotosetComment.
    """
    def __init__(self, *args, **kwargs):
        if '_content' in kwargs:
            kwargs['comment_text'] = kwargs['_content']
            del kwargs['_content']
        super(Comment, self).__init__(*args, **kwargs)
        self._params = {'comment_id':self.id}

    def delete(self, api_call, **params):
        return self._action(self._delete_f, params)

    def edit(self, api_call, comment_text='', **params):
        params = merge(dict(comment_text=comment_text),params)
        response = self._action(self._edit_f, params)
        self.comment_text = params['comment_text']
        return response

    def set_comment_text(self, value):
        return self.edit(comment_text=value)


class PhotoComment(Comment):
    _cacheable = True
    def edit(self, comment_text='', **params):
        """
        Edit this comment by replacing the current text with `comment_text`.
        You can even edit other people's comments on your photos.

        api: flickr.photos.comments.editComment

        """
        api_call = self.flickr.photos.comments.editComment
        return super(PhotoComment, self).edit(api_call, comment_text=comment_text, **params)

    def delete(self, **params):
        """
        Delete this comment.

        api: flickr.photos.comments.deleteComment

        """
        api_call = self.flickr.photos.comments.deleteComment
        return super(PhotoComment, self).edit(api_call, **params)


class PhotosetComment(Comment):
    _cacheable = True
    def edit(self, comment_text='', **params):
        """
        Edit this comment by replacing the current text with `comment_text`.

        api: flickr.photosets.comments.editComment

        """
        api_call = self.flickr.photosets.comments.editComment
        return super(PhotosetComment, self).edit(api_call, comment_text=comment_text, **params)

    def delete(self, **params):
        """
        Delete this comment.

        api: flickr.photosets.comments.deleteComment

        """
        api_call = self.flickr.photosets.comments.deleteComment
        return super(PhotosetComment, self).edit(api_call, **params)


class Group(BaseGlittr):
    _cacheable = True
    def __init__(self, *args, **kwargs):
        super(Group, self).__init__(*args, **kwargs)
        self._params = {'group_id':self.id}

    def add_photo(self, photo, **params):
        return self._action(self.flickr.groups.pools.add, params, objects=(photo,))

    def get_photos(self, **params):
        return self._action(self.flickr.groups.pools.getPhotos, params, 'photo', Photo, depaginate=True)

    def remove_photo(self, photo, **params):
        return self._action(self.flickr.groups.pools.remove, params, objects=(photo,))

    def previous_photos(self, photo, **params):
        return self._subsequent_photos('prev', self.flickr.groups.pools.getContext, photo, params)

    def next_photos(self, photo, **params):
        return self._subsequent_photos('next', self.flickr.groups.pools.getContext, photo, params)

    def get_info(self, **params):
        return self._action(self.flickr.groups.getInfo, ['group'], params)

    def get_url(self, **params):
        return self._action(self.flickr.urls.getGroup, ['group','url'], params)


class License(BaseGlittr):
    _cacheable = True
    """
    A simple class for holding Flickr licenses (used to indicate how photos
    are licensed).

    """
    def __init__(self, *args, **kwargs):
        super(License, self).__init__(*args, **kwargs)
        self._params = {'license_id':self.id}

    # TODO use getlicenses to get the info for a license

# TODO: how do people know what flickr method is being used? help for it?

class Note(BaseGlittr):
    _cacheable = True
    def __init__(self, *args, **kwargs):
        super(Note, self).__init__(*args, **kwargs)
        self._params = {'note_id':self.id}

    def edit(self, **params):
        """
        Coordinates and sizes are based on the 500px view shown on the normal
        photo page.

        Parameters are: note_x, note_y, note_w, note_h, note_text
        All parameters are required.

        """
        return self._action(self.flickr.photos.notes.edit, params)

    def delete(self, **params):
        return self._action(self.flickr.photos.notes.delete, params)


class Photo(BaseGlittr):
    _cacheable = True
    def __init__(self, *args, **kwargs):
        super(Photo, self).__init__(*args, **kwargs)
        self._params = {'photo_id':self.id}

    def get_contexts(self, **params):
        response = self._action(self.flickr.photos.getAllContexts, params)
        if response.has_key('pool'):
            r = response['pool']
            if isinstance(r, dict):
                yield Group(**r)
            else:
                for g in response['pool']:
                    yield Group(**g)
        if response.has_key('set'):
            r = response['set']
            if isinstance(r, dict):
                yield Photoset(**r)
            else:
                for s in r:
                    yield Photoset(**s)

    def add_comment(self, comment_text, **params):
        params = merge(dict(comment_text=comment_text),params)
        return self._action(self.flickr.photos.comments.addComment, params, ['comment'], PhotoComment)

    # TODO 'author' on comments ._info should be a User object
    def get_comments(self, **params):
        response = self._action(self.flickr.photos.comments.getList, params, 'comment', PhotoComment, depaginate=True)
        response = dates_filter(response)
        return response

    def get_favorites(self, **params):
        return self._action(self.flickr.photos.getFavorites, params, ['photo', 'person'], User, depaginate=True)

    def get_sizes(self, **params):
        return self._action(self.flickr.photos.getSizes, params, 'size', Size)

    def get_info(self, **params):
        response = self._action(self.flickr.photos.getInfo, params, ['photo'])
        response = dates_filter(response)
        response = keys_filter(response)
        response['license'] = License(response['license'])
        # TODO 'owner'
        return response

    def get_exif(self, **params):
        return self._action(self.flickr.photos.getExif, params, ['photo', 'exif'])

    def add_tags(self, *objects, **params):
        tags = ' '.join(str(tag) for tag in objects)
        params = merge(dict(tags=tags),params)
        return self._action(self.flickr.photos.addTags, params)

    def get_tags(self, **params):
        return self._action(self.flickr.tags.getListPhoto, params, ['photo','tags','tag'], Tag)

    def set_tags(self, *objects, **params):
        tags = ' '.join(str(tag) for tag in objects)
        params = merge(dict(tags=tags),params)
        return self._action(self.flickr.photos.setTags, params)

    def add_note(self, note_x, note_y, note_w, note_h, note_text, **params):
        """
        Coordinates and sizes are based on the 500px view shown on the normal
        photo page.

        """
        p = dict(note_x=note_x,note_y=note_y,note_h=note_h,note_w=note_w,note_text=note_text)
        params = merge(p,params)
        return self._action(self.flickr.photos.notes.add, params, ['note'], Note)

    def edit(self, **params):
        """
        `title` and `description`

        """
        if not params.has_key('title'):
            params['title'] = self.title
        elif not params.has_key('description'):
            params['description'] = self.description
        response = self._action(self.flickr.photos.setMeta, params)
        return response

    def set_title(self, value):
        return self.edit(title=value)

    def set_description(self, value):
        return self.edit(description=value)

#    def get_perms(self, **params):
#        response = self._action(self.flickr.photos.getPerms, params, ['perms'])
#        del response['id']
#        response = keys_filter(response)
#        return response

    def _set_perms(self, **params):
        """
        is_public=0, is_friend=0, is_family=0, perm_comment=0, perm_addmeta=0

        """
        params = merge(self.editability,self.visibility,params)
        return self._action(self.flickr.photos.setPerms, params)

    def set_visibility(self, **params):
        return self._set_perms(**params)

    def set_permissions(self, **params):
        return self._set_perms(**params)

    def get_geo_perms(self, **params):
        response = self._action(self.flickr.photos.geo.getPerms, params, ['perms'])
        del response['id']
        return response

    def set_geo_perms(self, **params):
        """
        is_public=None, is_contact=None, is_friend=None, is_family=None

        """
#        params = merge(self.perms,params)
        p = dict(is_public=is_public,is_contact=is_contact,is_friend=is_friend,is_family=is_family)
        params = merge(p,params)
        # a JSON response request is, for some reason, not allowed for this particular method
        params['format'] = 'rest'
        return self._action(self.flickr.photos.geo.setPerms, params)

    def get_geo_location(self, **params):
        return self._action(self.flickr.photos.geo.getLocation, params, ['photo','location'])

    def set_geo_location(self, latitude=0, longitude=0, **params):
        # TODO check for consistency
        params = merge(dict(lat=latitude,lon=longitude),params)
        return self._action(self.flickr.photos.geo.setLocation, params)

    def remove_geo_location(self, **params):
        return self._action(self.flickr.photos.geo.removeLocation, params)

    def set_dates(self, **params):
        return self._action(self.flickr.photos.setDates, params)

    def _remove_tag(self, tag, **params):
        # only works if the tag has a .unique_id attribute
        params = merge(dict(tag_id=tag.unique_id),params)
        return self._action(self.flickr.photos.removeTag, params)

    def remove_tags(self, *objects, **params):
        # this will get us tag objects with the proper ids
        tags = list(self.get_tags())
        for tag in [tag for tag in tags if tag in objects]:
            self._remove_tag(tag, **params)

    def get_license(self, *objects, **params):
        return License(id=self.get_info()['license'])

    def set_license(self, *objects, **params):
        return self._action(self.flickr.photos.licenses.setLicense, params)

    def get_previous_photos(self, **params):
        return self._subsequent_photos('prev', self.flickr.photos.getContext, self, params)

    def get_next_photos(self, **params):
        return self._subsequent_photos('next', self.flickr.photos.getContext, self, params)

    def delete(self, **params):
        return self._action(self.flickr.photos.delete, params)

    def rotate(self, degrees=90, **params):
        """
        Rotate the photo by `degrees` in [90, 180, 270]

        """
        params = merge(dict(degrees=degrees),params)
        return self._action(self.flickr.photos.transform.rotate, params)

    def replace(self, photo_file, **params):
        params['photo'] = photo_file
        return self._action(self.flickr.replace, params)

    @property # TODO rename, conflicts with photocontext
    def url(self):
        return "http://www.flickr.com/photos/%(owner)s/%(id)s" % {'owner':self.owner['nsid'], 'id':self.id}

    # TODO all URL shit
    @property
    def src(self):
        d = {'farm':self.farm, 'server':self.server, 'id':self.id, 'secret':self.secret}
        return "http://farm%(farm)s.static.flickr.com/%(server)s/%(id)s_%(secret)s.jpg" % d

    # TODO: original size/secret
    def src_size(self, size):
        """
        Size is one of m,s,t,b

        http://www.flickr.com/services/api/misc.urls.html
        """
        src = self.src
        return src[:-4] + "_" + size + src[-4:]

    def open(self):
        # TODO: open the url to the highest quality available or original as a file
        pass


class Photoset(BaseGlittr):
    _cacheable = True
    def __init__(self, *params, **kwargs):
        super(Photoset, self).__init__(*params, **kwargs)
        self._params = {'photoset_id':self.id}

    def add_photo(self, photo, **params):
        return self._action(self.flickr.photosets.addPhoto, params, objects=(photo,))

    def remove_photo(self, photo, **params):
        return self._action(self.flickr.photosets.removePhoto, params, objects=(photo,))

    def delete(self, **params):
        return self._action(self.flickr.photosets.delete, params)

    def edit(self, title=None, description=None, **params):
        params = merge(dict(title=title,description=description),params)
        if params['title'] is None:
            params['title'] = self.title
        elif params['description'] is None:
            params['description'] = self.description
        response = self._action(self.flickr.photosets.editMeta, params)
        self.title = params['title']
        self.description = params['description']
        return response

    def get_title(self):
        return self.__getattr__('title')

    def set_title(self, value):
        return self.edit(title=value)

    def get_description(self):
        return self.__getattr__('description')

    def set_description(self, value):
        return self.edit(description=value)

    def edit_photos(self, primary_photo, all_photos, **params):
        photo_ids = ','.join([photo.id for photo in all_photos])
        params = merge(dict(primary_photo_id=primary_photo.id,photo_ids=photo_ids),params)
        return self._action(self.flickr.photosets.editPhotos, params)

    def previous_photos(self, photo, **params):
        return self._subsequent_photos('prev', self.flickr.photosets.getContext, photo, params)

    def next_photos(self, photo, **params):
        return self._subsequent_photos('next', self.flickr.photosets.getContext, photo, params)

    def get_info(self, **params):
        response = self._action(self.flickr.photosets.getInfo, params, ['photoset'])
        return response

    def get_photos(self, **params):
        return self._action(self.flickr.photosets.getPhotos, params, ['photoset', 'photo'], Photo)

    def get_primary_photo(self, **params):
        return Photo(id=self.get_info()['primary'])

    def add_comment(self, comment_text, *objects, **params):
        params = merge(dict(comment_text=comment_text),params)
        response = self._action(self.flickr.photosets.comments.addComment, params, ['comment'])
        return PhotosetComment(**response)

    def get_comments(self, *objects, **params):
        return self._action(self.flickr.photosets.comments.getList, params, 'comment', PhotosetComment)
    
    
# TODO: url method? already included?
class Size(object):
    """
    An extremely simple class for holding the different available sizes of a
    photo.

    """
    def __init__(self, **kwargs):
        for k,v in kwargs.iteritems():
            try: setattr(self, k, v)
            except AttributeError: pass

    def __repr__(self):
        return "<Flickr Size: %s>" % self.label


class Tag(BaseGlittr):
    """
    A simple class for holding a tag.

    """
    # don't want the odd id behavior throwing things off
    # plus Tag objects don't have much state
    _cacheable = False
    def __init__(self, *args, **kwargs):
        # when you get the tags from a photo, they have a unique id number used
        # to delete them, since this conflicts with the tag name, and only
        # matters when tags are being deleted, it is given a special name
        if 'id' in kwargs:
            kwargs['unique_id'] = kwargs['id']
            del kwargs['id']
        if '_content' in kwargs:
            kwargs['id'] = kwargs['_content']
            del kwargs['_content']
        super(Tag, self).__init__(*args, **kwargs)
        self._params = {'tag':self.id}

    def get_raw(self, **params):
        """
        Only gets raw strings for the tag that the currently logged in user has used.

        """
        response = self._action(self.flickr.tags.getListUserRaw, params, ['who','tags','tag'])
        return iter(response[0]['raw'])

    def get_related(self, **params):
        response = self._action(self.flickr.tags.getRelated, params, ['tags', 'tag'])
        return (Tag(t) for t in response)

    def __str__(self):
        return self.id


class User(BaseGlittr):
    _cacheable = True
    def __init__(self, *params, **kwargs):
        super(User, self).__init__(*params, **kwargs)
        self._params = {'user_id':self.id}

    def _require_current_user(self):
        if self == self._get_current_user():
            return True
        else:
            raise GlittrError, "This method can only be called on the user object of the currently logged-in user"

    def get_photos(self, **params):
        # TODO multiple date formats from datetime, licenses, 
        return self._action(self.flickr.photos.search, params, 'photo', Photo)

    def get_public_photos(self, **params):
        return self._action(self.flickr.people.getPublicPhotos, params, 'photo', Photo)

    def get_photosets(self, **params):
        return self._action(self.flickr.photosets.getList, params, 'photoset', Photoset)

    def get_setless_photos(self, **params):
        return self._action(self.flickr.photos.getNotInSet, params, 'photo', Photo)

    def get_untagged_photos(self, **params):
        return self._action(self.flickr.photos.getUntagged, params, 'photo', Photo)

    def get_geotagged_photos(self, **params):
        return self._action(self.flickr.photos.getWithGeoData, params, 'photo', Photo)

    def get_ungeotagged_photos(self, **params):
        return self._action(self.flickr.photos.getWithoutGeoData, params, 'photo', Photo)

    def get_updated_photos(self, min_date, **params):
        params = merge(dict(min_date=min_date),params)
        self._require_current_user()
        return self._action(self.flickr.photos.recentlyUpdated, params, 'photo', Photo)

    def get_public_groups(self, **params):
        return self._action(params, self.flickr.people.getPublicGroups, params, 'group', Group)

    def get_postable_groups(self, **params):
        self._require_current_user()
        return self._action(self.flickr.groups.pools.getGroups, params, 'group', Group)

    def get_public_favorites(self, **params):
        return self._action(self.flickr.favorites.getPublicList, params, 'photo', Photo)

    def get_favorites(self, **params):
        return self._action(self.flickr.favorites.getList, params, 'photo', Photo)

    def get_public_contacts(self, **params):
        return self._action(self.flickr.contacts.getPublicList, params, 'contact', User)

    def get_contacts(self, **params):
        self._require_current_user()
        return self._action(self.flickr.contacts.getList, params, 'contact', User)

    def get_contacts_photos(self, **params):
        self._require_current_user()
        return self._action(self.flickr.photos.getContactsPhotos, params, 'photo', Photo)

    def get_contacts_public_photos(self, **params):
        self._require_current_user()
        return self._action(self.flickr.photos.getContactsPublicPhotos, params, 'photo', Photo)

    def get_blogs(self, **params):
        self._require_current_user()
        return self._action(self.flickr.blogs.getList, params, 'blog', Blog)

    def get_popular_tags(self, **params):
        return self._action(self.flickr.tags.getListUserPopular, params, ['who','tags','tag'], Tag)

    def get_tags(self, **params):
        response = self._action(self.flickr.tags.getListUser, params, ['who','tags','tag'])
        return (Tag(x) for x in response)

    def get_photos_url(self, **params):
        return self._action(self.flickr.urls.getUserPhotos, params, ['user','url'])

    def get_profile_url(self, **params):
        return self._action(self.flickr.urls.getUserProfile, params, ['user','url'])

    def get_info(self, **params):
        response = self._action(self.flickr.people.getInfo, params, ['person'])
        response = dates_filter(response)
        return response

    def get_counts(self, **params):
        """
        Pass in multiple datetime objects and get back the number of photos
        added in the time periods between the dates.

        """
        dates = ','.join([to_timestamp(t) for t in sorted(objects)])
        params = merge(dict(dates=dates),params)
        self._require_current_user()
        response = self._action(self.flickr.photos.getCounts, params, ['photocounts','photocount'])
        return response

    def add_favorite(self, **params):
        return self._action(self.flickr.favorites.add, params)

    def remove_favorite(self, **params):
        return self._action(self.flickr.favorites.remove, params)

    def upload_photo(self, photo, **params):
        """
        Upload `photo` to Flickr, 

        """
        params = merge(dict(photo=photo),params)
        self._require_current_user()
        response = self._action(self.flickr.upload, params)
        return Photo(id=response['photoid'])

    def create_photoset(self, primary_photo, title='', **params):
        """
        Create a new photoset with `title`, using `primary_photo` as the photo
        to represent the set.

        """
        params = merge(dict(primary_photo_id=primary_photo.id,title=title),params)
        self._require_current_user()
        response = self._action(self.flickr.photosets.create, params)
        return Photoset(**response['photoset'])

    def order_photosets(self, photosets, **params):
        """
        Any photosets not in the list will appear at the end, ordered by their
        ids.
    
        """
        photoset_ids = ','.join([photoset.id for photoset in photosets])
        params = merge(dict(photoset_ids=photoset_ids),params)
        self._require_current_user()
        return self._action(self.flickr.photosets.orderSets, params)

    def get_groups(self, **params):
        """
        Get both public and postable groups 
        """
        public_groups = self.get_public_groups(**params)
        try:
            self._require_current_user()
        except GlittrError:
            return public_groups
        else:
            postable_groups = self.get_postable_groups(**params)
            return iter(set(list(public_groups) + list(postable_groups)))

